#!/usr/bin/env bash

##
## A minimalistic MacPorts Package Manager for OSXCross,
## based on: https://github.com/maci0/pmmacports.
## Please see README.MACPORTS for more.
## License: GPLv2.
##

set -e
export LC_ALL=C
pushd "${0%/*}" &>/dev/null

PUBKEYURL="https://svn.macports.org/repository/"
PUBKEYURL+="macports/trunk/base/macports-pubkey.pem"

# Darwin's antique OpenSSL does not support the SHA-2 family
PUBKEYRMD160="d3a22f5be7184d6575afcc1be6fdb82fd25562e8"
PUBKEYSHA1="214baa965af76ff71187e6c1ac91c559547f48ab"

PLATFORM=$(uname -s)

if [ -z "$BASHPID" ]; then
  BASHPID=$(sh -c 'echo $PPID')
fi

errorMsg()
{
  echo "$@" 1>&2
}

verboseMsg()
{
  if [ -n "$VERBOSE" ]; then
    errorMsg "$@"
  fi
}

verbosePlaceHolder()
{
  if [ -n "$VERBOSE" ]; then
    errorMsg ""
  fi
}

require()
{
  set +e
  command -v $1 &>/dev/null
  if [ $? -ne 0 ]; then
    errorMsg "$1 is required"
    exit 1
  fi
  set -e
}

if [ -z "$MACOSX_DEPLOYMENT_TARGET" ]; then
  errorMsg "you must set MACOSX_DEPLOYMENT_TARGET first."
  errorMsg "please see README.MACPORTS."
  exit 1
fi

unsupportedDepTarget()
{
  errorMsg "unsupported deployment target"
  exit 1
}

if uname | grep -qi bsd; then
  SORT="gsort"
else
  SORT="sort"
fi

require $SORT
require "openssl"
require "bzip2"
require "tar"

# Prefer bsdtar (handles SCHILY.* pax headers quietly); fall back to GNU tar with warning suppression.
TAR_CMD=(tar)
if command -v bsdtar &>/dev/null; then
  TAR_CMD=(bsdtar)
elif tar --version 2>/dev/null | grep -q "GNU tar"; then
  TAR_CMD=(tar --warning=no-unknown-keyword)
fi

# Set ARCH based on MACOSX_DEPLOYMENT_TARGET
major="${MACOSX_DEPLOYMENT_TARGET%%.*}"
minor="${MACOSX_DEPLOYMENT_TARGET#*.}"

if [ "$major" -lt 10 ] || { [ "$major" -eq 10 ] && [ "$minor" -le 16 ]; }; then
  ARCH="x86_64"
else
  ARCH="arm64"
fi

case $MACOSX_DEPLOYMENT_TARGET in
  10.6*  ) OSXVERSION="darwin_10"; UNIVERSAL_ARCHS="i386-x86_64" ;;
  10.7*  ) OSXVERSION="darwin_11"; UNIVERSAL_ARCHS="i386-x86_64"  ;;
  10.8*  ) OSXVERSION="darwin_12"; UNIVERSAL_ARCHS="x86_64" ;;
  10.9*  ) OSXVERSION="darwin_13"; UNIVERSAL_ARCHS="x86_64" ;;
  10.10* ) OSXVERSION="darwin_14"; UNIVERSAL_ARCHS="x86_64" ;;
  10.11* ) OSXVERSION="darwin_15"; UNIVERSAL_ARCHS="x86_64" ;;
  10.12* ) OSXVERSION="darwin_16"; UNIVERSAL_ARCHS="x86_64" ;;
  10.13* ) OSXVERSION="darwin_17"; UNIVERSAL_ARCHS="x86_64" ;;
  10.14* ) OSXVERSION="darwin_18"; UNIVERSAL_ARCHS="x86_64" ;;
  10.15* ) OSXVERSION="darwin_19"; UNIVERSAL_ARCHS="x86_64" ;;
  10.16* ) OSXVERSION="darwin_20"; UNIVERSAL_ARCHS="x86_64-arm64" ;;
  11|11.* ) OSXVERSION="darwin_20"; UNIVERSAL_ARCHS="x86_64-arm64" ;;
  12|12.* ) OSXVERSION="darwin_21"; UNIVERSAL_ARCHS="x86_64-arm64" ;;
  13|13.* ) OSXVERSION="darwin_22"; UNIVERSAL_ARCHS="x86_64-arm64" ;;
  14|14.* ) OSXVERSION="darwin_23"; UNIVERSAL_ARCHS="x86_64-arm64" ;;
  15|15.* ) OSXVERSION="darwin_24"; UNIVERSAL_ARCHS="x86_64-arm64" ;;
  26|26.* ) OSXVERSION="darwin_25"; UNIVERSAL_ARCHS="x86_64-arm64" ;;
    * ) unsupportedDepTarget ;;
esac

require "osxcross-conf"
eval $(osxcross-conf)

if [ -z "$OSXCROSS_TARGET_DIR" ]; then
  errorMsg "OSXCROSS_TARGET_DIR should be set"
  exit 1
fi

ROOT="$OSXCROSS_TARGET_DIR/macports"

if [ ! -d "$ROOT" ]; then
  echo "creating macports directory: $ROOT"
  mkdir -p $ROOT
fi

PUBKEY="$ROOT/mp-pubkey.pem"
INSTALLDB="$ROOT/INSTALLED"
SELECTEDMIRROR="$ROOT/MIRROR"
CACHE="$ROOT/cache"
INSTALL="$ROOT/pkgs"
TMP="$ROOT/tmp"
LASTPKGNAME=""

LOCKDIR="/tmp/osxcross-macports-lock"
LOCKPID="$LOCKDIR/pid"

checkLock()
{
  if mkdir $LOCKDIR 2>/dev/null; then
    echo $BASHPID > $LOCKPID
  else
    pid=$(cat $LOCKPID 2>/dev/null) || { errorMsg "remove $LOCKDIR"; exit 1; }
    if [ $? -eq 0 -a "$(ps -p $pid -o comm=)" == "bash" ]; then
      errorMsg "locked by pid $pid"
    else
      errorMsg "dead lockdir detected! please remove '$LOCKDIR' by hand."
    fi
    exit 1
  fi
}

createDir()
{
  if [ ! -d $2 ]; then
    echo "creating $1 directory: $2"
    mkdir -p $2
  fi
}

checkLock

createDir "cache" $CACHE
createDir "install" $INSTALL
createDir "tmp" $TMP

selectMirror()
{
  echo "available mirrors:"
  echo ""
  echo "1: (https) packages.macports.org (United States)"
  echo "2: (http)  mirrorservice.org (United Kingdom)"
  echo "3: (https) nue.de.packages.macports.org (Germany)"
  echo "4: (https) lil.fr.packages.macports.org (France)"
  echo ""

  local OK=0
  local mirrorNumber

  while [ $OK -ne 1 ]; do
    echo -n "please enter the number of the mirror you want to use: "

    if [ -n "$UNATTENDED" ]; then
      echo "1"
      mirrorNumber=1
    else
      read mirrorNumber
    fi

    case $mirrorNumber in
      1) echo -n "https://packages.macports.org" > \
          $SELECTEDMIRROR && OK=1 ;;
      2) echo -n "http://www.mirrorservice.org/sites/packages.macports.org" > \
          $SELECTEDMIRROR && OK=1 ;;
      3) echo -n "https://nue.de.packages.macports.org/macports/packages" > \
          $SELECTEDMIRROR && OK=1 ;;
      4) echo -n "https://lil.fr.packages.macports.org" > \
          $SELECTEDMIRROR && OK=1 ;;
      *) echo "please enter a number between 1 and 4..." 1>&2 ;;
    esac
  done

  MIRROR=$(cat $SELECTEDMIRROR)
}

function download()
{
  local uri=$1
  local filename

  if [ $# -eq 2 ]; then
    filename=$2
  else
    filename=$(basename $1)
  fi

  if command -v curl &>/dev/null; then
    ## cURL ##
    local curl_opts="-L -C - "
    if [ -z "$VERBOSE" ]; then
      curl_opts+="-s "
    fi
    if [ $filename != "-" ]; then
      curl_opts+="-o $filename "
    fi
    curl $curl_opts $uri
  elif command -v wget &>/dev/null; then
    ## wget ##
    local wget_opts="-c -O $filename "
    local output=$(wget --no-config 2>&1)
    if [[ $output != *--no-config* ]]; then
      wget_opts+="--no-config "
    fi
    if [ $PLATFORM == "FreeBSD" ]; then
      wget_opts="--ca-certificate="
      wget_opts+="/usr/local/share/certs/ca-root-nss.crt "
    fi
    if [ -z "$VERBOSE" ]; then
      wget_opts+="--quiet "
    fi
    wget $wget_opts $uri
  else
    echo "Required dependency 'curl or wget' not installed" 1>&2
    exit 1
  fi
}

getFileStdout()
{
  verbosePlaceHolder
  download $1 -
  #verbosePlaceHolder
}

getFile()
{
  verbosePlaceHolder
  if [ $# -ge 2 ]; then
    download $1 $2
  else
    download $1 "$CACHE/$(basename $1)"
  fi
  #verbosePlaceHolder
}

verifyFileIntegrity()
{
  local file="$1"

  if [ ! -e "$PUBKEY" ]; then
    echo "getting macports public key ..."
    getFile $PUBKEYURL "$PUBKEY"
  fi

  local rmd160=$(openssl rmd160 "$PUBKEY" | awk '{print $2}')
  local sha1=$(openssl sha1 "$PUBKEY" | awk '{print $2}')

  if [ "$rmd160" != "$PUBKEYRMD160" -o "$sha1" != "$PUBKEYSHA1" ]; then
    errorMsg "invalid macports public key (hash check failed)"
    exit 1
  fi

  echo "verifying file integrity of $file ..."

  set +e

  openssl dgst -ripemd160 -verify "$PUBKEY" -signature \
    "$CACHE/$file.rmd160" "$CACHE/$file" 1>/dev/null

  if [ $? -ne 0 ]; then
    errorMsg "file integrity check failed ($CACHE/$file)"
    exit 2
  fi

  set -e
}

logPkgCandidates() {
  local context="$1"
  shift
  local pkgs="$@"
  local count=$(echo "$pkgs" | wc -w)
  local list_pkgs=""
  if [ $count -lt 5 ]; then
    list_pkgs=$(echo "$pkgs")
  fi

  verboseMsg "  ${context}: ${count} packages left.. ${list_pkgs}"
}

supportsRequestedArches() {
  local candidate_arch="$1"
  local requested_arches="$2"
  local requested=()
  local candidate_list=()

  # Architecture independent packages always work.
  if [ "$candidate_arch" == "noarch" ]; then
    return 0
  fi

  IFS='-' read -ra requested <<< "$requested_arches"
  IFS='-' read -ra candidate_list <<< "$candidate_arch"

  for req in "${requested[@]}"; do
    local found=1
    for cand in "${candidate_list[@]}"; do
      if [ "$req" == "$cand" ]; then
        found=0
        break
      fi
    done
    if [ $found -ne 0 ]; then
      return 1
    fi
  done

  return 0
}

getPkgUrl()
{
  local pkgname="$1"
  local pkgversion
  local pkgs
  local pkg

  set +e

  local pkg_info_url="https://ports.macports.org"
  pkg_info_url+="/api/v1/ports/$pkgname/?format=json"
  pkgversion=$(getFileStdout "$pkg_info_url" | \
               grep -o -E '"version":"[^"]+"' | \
               cut -d'"' -f4)

  pkgs=$(getFileStdout "$MIRROR/$pkgname/?C=M;O=A" | \
         grep -o -E 'href="([^"#]+)"' | \
         cut -d'"' -f2 | grep '.tbz2$' | uniq)

  local ec=$?

  set -e

  if [ $ec -ne 0 ]; then
    errorMsg "no package found"
    return
  fi

  verboseMsg " candidates for $pkgname:"

  for p in $pkgs; do
    verboseMsg "  $p"
  done

  # Find the newest version that supports every requested architecture. We
  # still try the API-advertised version first, then fall back to older ones.
  local os_filtered=$(echo "$pkgs" | grep -E "\.(${OSXVERSION}|darwin_any|any_any)\.")
  local versions=$(echo "$os_filtered" | \
                   sed -E "s/^${pkgname}-//" | \
                   sed -E 's/\.darwin.*//' | \
                   sed -E 's/\+.*//' | $SORT -u -V)

  local ordered_versions=()
  if [ -n "$pkgversion" ]; then
    ordered_versions+=("$pkgversion")
  fi
  while read -r v; do
    if [ "$v" != "$pkgversion" ]; then
      ordered_versions+=("$v")
    fi
  done < <(echo "$versions" | $SORT -rV)

  for version in "${ordered_versions[@]}"; do
    [ -z "$version" ] && continue

    local step1=$(echo "$pkgs" | grep "$pkgname-$version")
    logPkgCandidates "step1" $step1

    local step2=$(echo "$step1" | grep -E "\.(${OSXVERSION}|darwin_any|any_any)\.")
    logPkgCandidates "step2" $step2

    local best_pkg=""
    local best_score=-1
    local step3=""

    for candidate in $step2; do
      # Extract the arch suffix regardless of darwin/any prefix.
      local arch=$(echo "$candidate" | sed -n 's/.*\.\([^.]*\)\.tbz2/\1/p')

      if [ -z "$arch" ]; then
        continue
      fi

      if ! supportsRequestedArches "$arch" "$ARCH"; then
        continue
      fi

      step3+="$candidate "

      # Prefer an exact arch match, then fat/universal that include all arches,
      # and finally noarch.
      local score=1
      if [ "$arch" == "$ARCH" ]; then
        score=3
      elif [ "$arch" != "noarch" ]; then
        score=2
      fi

      if [ $score -gt $best_score ]; then
        best_score=$score
        best_pkg=$candidate
      fi
    done

    logPkgCandidates "step3" $step3

    if [ -n "$best_pkg" ]; then
      pkg=$best_pkg
      break
    fi
  done

  verboseMsg " selected: $pkg"

  if [ -z "$pkg" ]; then
    verboseMsg -n "  "
    errorMsg "no suitable version found for $OSXVERSION on $ARCH"
    return
  fi

  echo "$MIRROR/$pkgname/$pkg"
}

pkgInstalled()
{
  local pkgname="$1"

  if [ ! -e "$INSTALLDB" ]; then
    echo 0
    return
  fi

  set +e
  grep -x "$pkgname" "$INSTALLDB" &>/dev/null
  local status=$?
  set -e

  if [ $status -eq 0 ]; then
    echo 1
  else
    echo 0
  fi
}

installPkg()
{
  local pkgname="$1"

  LASTPKGNAME=$pkgname

  if [ $(pkgInstalled $pkgname) -eq 1 ]; then
    return
  fi

  echo "searching package $pkgname ..."

  local pkgurl=$(getPkgUrl "$pkgname")
  local pkgfile=$(echo "$pkgurl" | awk -F'/' '{print $NF}')

  if [ -z "$pkgurl" ]; then
    local oldpkgname=$pkgname
    local tmp=$(echo "$pkgname" | awk -F'-'  '{print $NF}')
    [ -n "$tmp" ] && pkgname=${pkgname/-$tmp/}

    if [ "$pkgname" != "$oldpkgname" ]; then
      echo "trying $pkgname instead ..."
    else
      [ -n "$UPGRADE" ] && return
      exit 3
    fi

    installPkg $pkgname
    return
  fi

  echo "getting $pkgfile ..."
  getFile "$pkgurl"

  verboseMsg "getting $pkgname.rmd160 ..."
  getFile "$pkgurl.rmd160"

  verifyFileIntegrity "$pkgfile"

  pushd $TMP &>/dev/null

  echo "installing $pkgname ..."
  verboseMsg " extracting $pkgfile ..."

  bzip2 -dc "$CACHE/$pkgfile" | "${TAR_CMD[@]}" -xf -

  if [ -d opt/local ]; then
    verboseMsg " fixing permissions ..."
    find opt/local -type d -exec chmod 755 {} \;
    find opt/local -type f -exec chmod 644 {} \;
    if [ -d opt/local/lib ]; then
      if [ -n "$STATIC" ]; then
        verboseMsg " "
        echo "removing dylibs ..."
        find opt/local/lib -name "*.dylib" -exec rm {} \;
      else
        find opt/local/lib -type f -name "*.dylib" -exec chmod +x {} \;
      fi
    fi
    if [ -d opt/local/bin ]; then
      find opt/local/bin -type f -exec chmod +x {} \;
    fi
    set +e
    cp -r opt $INSTALL
    local status=$?
    set -e
    if [ $status -eq 1 ]; then
      errorMsg "removing broken symlinks ..."
      find -L . -type l -exec rm {} \;
      cp -r opt $INSTALL
    fi
  fi

  local pkgdeps=$(grep '@pkgdep' \+CONTENTS | cut -d\  -f2 | \
                  rev | cut -f2-100 -d\- | rev)

  popd &>/dev/null # TMP
  rm -rf $TMP/*

  for pkgdep in $pkgdeps; do
    installPkg $pkgdep
  done

  echo "$pkgname" >> "$INSTALLDB"
}

installPkgs()
{
  local packages="$1"

  for pkgname in $packages; do
    if [ $(pkgInstalled $pkgname) == "1" ]; then
      errorMsg "$pkgname is already installed"
      continue
    fi
    installPkg $pkgname
    echo "installed $pkgname"
    rm -f $INSTALL/+*
  done
}

updateSearchCache()
{
  pushd $CACHE &>/dev/null

  echo "generating index cache (this may take several minutes ...)"
  getFile $MIRROR $CACHE/packages

  cat packages | grep -o -E 'href="([^"#]+)"' | cut -d'"' -f2 | \
    sed 's/.\{1\}$//' | uniq | $SORT > INDEXCACHE

  rm -f packages
  echo "generated index cache for $(cat INDEXCACHE | wc -l) packages"

  popd &>/dev/null
}

searchPkg()
{
  local pkg="$1"

  pushd $CACHE &>/dev/null

  if [ ! -e INDEXCACHE ]; then
    updateSearchCache
  fi

  local packages=$(grep -i "$pkg" INDEXCACHE)

  if [ -z "$packages" ]; then
    echo "no matching packages found for $1"
    return
  fi

  for pkg in $packages; do
    echo $pkg
  done

  popd &>/dev/null
}

_exit()
{
  local ec=$?
  if [ $ec -ne 0 ]; then
    if [ -n "$LASTPKGNAME" ]; then
      errorMsg "failed to install $LASTPKGNAME, try with '-v'"
    fi
    if [ $ec -eq 3 ]; then
      errorMsg -n "use '$(basename $0) fake-install <pkgname>' to "
      errorMsg "fake install non-existing packages"
    fi
  fi

  rm -rf $LOCKDIR
}

showFlags()
{
  if [ -n "$OSXCROSS_TARGET" ]; then
    PKG_CONFIG="x86_64-apple-${OSXCROSS_TARGET}-pkg-config"
  else
    PKG_CONFIG="pkg-config"
  fi

  $PKG_CONFIG $1 $2
}

showCFLAGS()
{
  showFlags "--cflags" $1
}

showLDFLAGS()
{
  showFlags "--libs" $1
}

upgrade()
{
  if [ ! -e $INSTALLDB ]; then
    if [ -e $INSTALLDB.old ]; then
      echo "restoring old installation database"
      mv $INSTALLDB.old $INSTALLDB
    else
      errorMsg "no install database"
      exit 1
    fi
  fi

  local PKGS=$(cat $INSTALLDB)
  mv $INSTALLDB $INSTALLDB.old

  UPGRADE=1
  rm -rf $INSTALL
  createDir "" $INSTALL 1>/dev/null

  for pkg in $PKGS; do
    installPkg $pkg
  done
}

clearCache()
{
  rm -rf $CACHE
}

removeDylibs()
{
  find $INSTALL -name "*.dylib" -exec rm {} \;
}

showHelpText()
{
  errorMsg "please see README.MACPORTS"
}

main()
{
  local args
  local cmd

  MIRROR="$OSXCROSS_MACPORTS_MIRROR"

  if [ -z "$MIRROR" ] && [ -f "$SELECTEDMIRROR" ]; then
    MIRROR=$(cat $SELECTEDMIRROR | head -n1)
  fi

  if [ -z "$MIRROR" ]; then
    selectMirror
  fi

  for opt in $@; do
    if [[ $opt == -* ]]; then
      if [ $opt == "-v" -o $opt == "--verbose" ]; then
        VERBOSE=1
      elif [ $opt == "-v=2" -o $opt == "--verbose=2" ]; then
        set -x
        VERBOSE=1
      elif [ $opt == "-s" -o $opt == "--static" ]; then
        STATIC=1
      elif [ $opt == "-c" -o $opt == "--cflags" ]; then
        showCFLAGS $2
        exit
      elif [ $opt == "-l" -o $opt == "--ldflags" ]; then
        showLDFLAGS $2
        exit
      elif [ $opt == "-x86_64" -o $opt == "--x86_64" -o $opt == "-x86-64" -o $opt == "--x86-64" ]; then
        ARCH="x86_64"
      elif [ $opt == "-32" -o $opt == "--i386" ]; then
        ARCH="i386"
      elif [ $opt == "-universal" -o $opt == "--universal" ]; then
        ARCH=${UNIVERSAL_ARCHS}
      elif [ $opt == "-arm64" -o $opt == "--arm64" ]; then
        ARCH="arm64"
      elif [ $opt == "-sm" -o $opt == "--select-mirror" ]; then
        selectMirror
        exit
      elif [ $opt == "-h" -o $opt == "--help" ]; then
        showHelpText
        exit
      else
        errorMsg "unknown option: $opt"
        exit 1
      fi
    else
      if [ -z "$cmd" ]; then
        cmd="$opt"
      else
        args+="$opt "
      fi
    fi
  done

  if [ -z "$cmd" ]; then
    errorMsg "no command given"
    showHelpText
    exit 1
  fi

  case "$cmd" in
    update*cache )
      updateSearchCache
      exit
    ;;

    upgrade )
      upgrade
      exit
    ;;

    clear*cache )
      clearCache
      echo "done"
      exit
    ;;

    remove*dylibs )
      removeDylibs
      echo "done"
      exit
    ;;

  esac

  local packages="$args"

  if [ -z "$packages" ]; then
    errorMsg "no package name given"
    exit 1
  fi

  case "$cmd" in
    install )
      installPkgs "$packages"
    ;;

    fake*install )
      for pkgname in $args; do
        if [ $(pkgInstalled $pkgname) -eq 0 ]; then
          echo $pkgname >> $INSTALLDB
        fi
      done

      echo "done"
    ;;

    search )
      for pkgname in $args; do
        searchPkg $pkgname
      done
    ;;

    * )
      showHelpText
    ;;
  esac
}

trap '' TERM
trap '' INT

trap _exit EXIT

main "$@"
